10장. 앱 한 겹씩 쌓기와 사용자 반응 줍기

출처: 『AI 엔지니어링』(Chip Huyen 지음, 한국어판) | 원서: AI Engineering (O'Reilly)

코드는 분위기만 — Python·함수 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.

지금까지 우리는 모델을 가져다(파운데이션 모델), 말로 시키고(프롬프트), 자료를 붙이고(RAG), 채점하고(평가), 다듬고(파인튜닝), 운영하는(추론) 큰 줄기를 봤다.

이 장은 그 조각들을 어떻게 한 겹씩 쌓아 진짜 앱으로 만드는지, 그리고 사용자가 남기는 반응을 어떻게 주워 모델을 더 좋게 만드는지 다룬다.

처음부터 다 만들려고 하면 무너진다.

그래서 가장 단순한 한 줄에서 시작해 필요할 때마다 한 겹씩 덧댄다.


0. 이 장의 새 단어 (0장에 없는 말만)

0장 용어집에 있는 말(파운데이션 모델·프롬프트·RAG·평가·추론·환각 등)은 이미 안다고 치고 간다.

이 장에서 처음 나오는 말은 딱 3개다.

각 단어는 [한 문장 뜻 + 일상비유 + 한 줄 예] 3종으로 적었다.


가드레일(guardrail)

한 문장 뜻 — 모델에 들어가는 입력과 나오는 출력을 검사해서, 위험한 것을 막거나 가려 주는 보호 장치.

일상비유 — 볼링장의 옆 홈 막이(범퍼). 공이 도랑으로 빠지지 않게 양옆을 막아 준다. 위험한 입력이 들어가는 것도, 위험한 답이 나오는 것도 막는다.

한 줄 예 —

# 들어가는 글에서 위험한 정보를 가린 뒤에 모델에 넘김
safe_input = mask_secrets(user_text)   # 전화번호 같은 걸 가림
answer = model.ask(safe_input)

라우터(router)

한 문장 뜻 — 들어온 질문이 어떤 종류인지 보고, 가장 알맞은 곳으로 보내 주는 교통정리 담당.

일상비유 — 병원 접수처. "배가 아파요" 는 내과로, "이가 아파요" 는 치과로 보낸다. 모든 사람을 한 의사에게 몰지 않고, 증상에 맞는 곳으로 나눠 준다.

한 줄 예 —

# 질문 종류를 보고 알맞은 처리로 보냄
kind = classify(question)        # 예: "환불" / "기술문제"
send_to(kind)                    # 그에 맞는 곳으로 전달

캐싱(caching)

한 문장 뜻 — 한 번 만든 답을 적어 뒀다가, 같은 질문이 또 오면 다시 만들지 않고 적어 둔 답을 꺼내 주는 것.

일상비유 — 자주 쓰는 답을 붙여 둔 메모지. 손님이 "영업시간이요?" 물을 때마다 매번 찾지 않고, 벽에 붙여 둔 메모를 그대로 읽어 준다. 훨씬 빠르고 힘이 덜 든다.

한 줄 예 —

# 적어 둔 답이 있으면 그걸 쓰고, 없을 때만 새로 만듦
if question in memo:
    return memo[question]        # 적어 둔 답 재사용

귀납 도입을 하나 보자.

어떤 팀이 첫 AI 앱을 만들었다.

야심 차게 가드레일, 라우터, 캐싱, 에이전트를 한꺼번에 다 넣은 거대한 설계도를 그렸다.

3개월이 지났는데 아직 첫 화면도 못 띄웠다.

옆 팀은 그냥 "질문 받아서 모델에 넘기고 답 돌려주기" 딱 한 줄로 시작했다.

# 가장 단순한 AI 앱 — 이게 전부다
answer = model.ask(question)
return answer

옆 팀은 일주일 만에 사용자를 받았다.

그리고 진짜 문제가 보일 때마다 한 겹씩 덧댔다.

전화번호가 새는 게 보이니 가드레일을 한 겹, 질문 종류가 뒤섞여 헷갈리니 라우터를 한 겹.

처음부터 다 쌓으려다 무너지는 것 — 그게 이 장이 막으려는 첫 번째 고통이다.

한 줄에서 시작해 필요할 때 한 겹씩.

이게 이 장의 첫 번째 큰 줄기다.


이 장에서 딱 4가지만

이 장에서 딱 4가지만

  1. 한 겹씩 쌓기 — 다 만들려 말고, "질문→답" 한 줄에서 시작해 필요할 때 가드레일·라우터·캐싱을 한 겹씩 덧댄다.
  2. 가드레일 — 들어가는 입력과 나오는 출력을 검사해, 위험한 정보가 새거나 나쁜 답이 나가는 걸 막는 옆 홈 막이.
  3. 반응 줍기 — 사용자는 별점뿐 아니라 행동(고쳐 쓰기·다시 만들기·그냥 나가기)으로도 말한다. 이 행동이 모델을 키우는 자료다.
  4. 반응은 비뚤다 — 사람은 미안해서 후하게 주고, 첫째 것을 더 누르고, 듣고 싶은 말을 한 모델을 더 좋아한다. 그대로 믿으면 안 된다.

아래에서 이 4가지를 하나씩, 망가지는 장면부터 본다.


1. 한 겹씩 쌓기 — 다 만들지 말고 한 줄에서 시작

망가지는 장면

설계도에 라우터, 가드레일, 캐싱, 에이전트를 다 그려 넣었다.

부품이 너무 많아 어디서 고장 났는지 찾을 수가 없다.

한 줄짜리 앱이었다면 1초 만에 알았을 문제를, 거대한 설계 속에서 사흘을 헤맸다.

일상비유

집을 지을 때 처음부터 3층에 수영장에 엘리베이터를 다 그리지 않는다.

먼저 방 한 칸을 올려 사람이 살아 보게 한다.

비가 새면 지붕을 한 겹, 추우면 벽을 한 겹 덧댄다.

AI 앱도 똑같다. "질문→답" 방 한 칸부터 올린다.

비유 코드 위험
방 한 칸부터 (질문→답 한 줄) answer = model.ask(question) 안전 — 어디가 고장 나도 한눈에 보임
처음부터 다 짓기 (전부 한꺼번에) app = build_all(guard, router, cache, agent) 부품 폭증 — 고장 위치 못 찾음, 출시도 못 함

한 문장 정의 — 한 겹씩 쌓기는 가장 단순한 "질문→답"에서 시작해, 진짜 문제가 보일 때마다 가드레일·라우터·캐싱 같은 겹을 하나씩 덧대는 방식이다.

예시 폭격

예시 1 — 완성예 (보면서 따라 이해)

순서가 정해져 있다. 아래 다섯 겹을 위에서부터 차례로 얹는다.

# 1겹: 질문 받아 답하기 (시작점)
answer = model.ask(question)

# 2겹: 자료 붙이기 (RAG) — 답이 자꾸 틀릴 때
answer = model.ask(question + 찾아온_문서)

# 3겹: 가드레일 — 정보가 새거나 나쁜 답이 나갈 때
answer = guard(model.ask(question + 찾아온_문서))

위로 갈수록 단순하고, 아래로 갈수록 한 겹씩 늘었다.

문제가 없으면 윗단계에서 멈춰도 된다.

예시 2 — 부분완성 (빈칸을 채워 보자)

세탁 앱을 만든다. 지금은 답이 가끔 회사에 없는 규정을 지어낸다(환각).

다음에 덧댈 겹은 무엇일까?

# 지금: 맨몸 질문
answer = model.ask(question)

# 덧대야 할 겹: 우리 규정 문서를 ____ 붙여서 보고 답하게
answer = model.ask(question + ________)   # 빈칸: 찾아온_문서

빈칸 답은 찾아온_문서다.

지어내는 문제니까 자료를 붙이는 겹(RAG)을 먼저 덧댄다.

예시 3 — 독립적용 (스스로 판단)

상황: 앱이 가끔 사용자 전화번호를 외부 모델에 그대로 보낸다.

이때 다음에 덧댈 겹은 RAG일까, 가드레일일까?

답은 가드레일이다.

정보가 새는 문제는 자료를 더 붙인다고 해결되지 않는다.

들어가는 입력을 검사해 가려 주는 겹이 필요하다.

미니 시나리오 — 이럴 때 이렇게

데모는 잘 돌아가는데 진짜 사용자에게 내보내기 겁날 때.

→ 한 번에 다 막으려 하지 말고, 가장 무서운 위험 하나만 골라 그 겹부터 덧대라.

가장 단순한 행동 규칙: 한 줄에서 시작한다. 진짜 문제가 보일 때만 한 겹 덧댄다.


2. 가드레일 — 새는 정보와 나쁜 답을 막는 옆 홈 막이

망가지는 장면

직원이 회사 기밀을 통째로 프롬프트에 붙여 외부 모델에 보냈다.

그 기밀이 외부 회사 서버에 그대로 넘어갔다.

반대로, 모델이 욕설 섞인 답을 사용자에게 그대로 내보내 항의가 빗발쳤다.

들어갈 때도 나올 때도 막는 게 없었던 것이다.

일상비유

볼링장 옆 홈 막이(범퍼).

들어갈 때 막이가 공을 도랑에서 지켜 주고(입력 검사), 나올 때 막이가 엉뚱한 레인으로 안 가게 한다(출력 검사).

들어가는 글과 나오는 글, 양쪽에 막이를 댄다.

비유 코드 위험
들어가는 막이 (입력 검사) safe = mask_secrets(user_text) 안전 — 전화번호·기밀이 새기 전에 가림
나오는 막이 (출력 검사) if is_bad(answer): retry() 안전 — 욕설·거짓 답을 내보내기 전에 거름
막이 없음 (그냥 통과) answer = model.ask(user_text) 정보 유출 + 나쁜 답 그대로 전달

한 문장 정의 — 가드레일은 모델에 들어가는 입력나오는 출력을 검사해, 위험한 정보가 새거나 나쁜 답이 나가는 것을 막는 양방향 보호 장치다.

예시 폭격

예시 1 — 완성예 (들어가는 막이가 하는 일)

들어가는 글에서 전화번호를 가림표로 바꾼다.

# 전화번호를 가림표로 바꿔서 외부에 안 새게 함
user_text = "내 번호는 010-1234-5678 이야"
safe = mask_secrets(user_text)   # "내 번호는 [전화번호] 이야"
answer = model.ask(safe)

진짜 번호는 외부로 나가지 않는다.

예시 2 — 부분완성 (나오는 막이를 채워 보자)

모델 답이 가끔 깨진 모양으로 나온다.

나올 때 검사해서, 나쁘면 한 번 더 물어보게 하자.

answer = model.ask(question)
# 답이 나쁘면 ____ 해서 다른 답을 받자
if is_bad(answer):
    answer = model.ask(question)   # 빈칸: 다시 물어보기(재시도)

빈칸은 다시 물어보기다.

모델은 매번 답이 조금씩 달라서, 다시 물으면 멀쩡한 답이 올 수 있다.

예시 3 — 독립적용 (어느 쪽 막이인가)

상황 A: 사용자가 비밀번호를 프롬프트에 적었다.

상황 B: 모델이 거짓 사실을 답으로 내놨다.

각각 어느 막이가 잡아야 할까?

A는 들어가는 막이(입력 검사)다. 새기 전에 가린다.

B는 나오는 막이(출력 검사)다. 나가기 전에 거른다.

미니 시나리오 — 이럴 때 이렇게

막이를 너무 빡빡하게 걸어 멀쩡한 질문까지 막혔을 때.

→ "막은 횟수"만 보지 말고 "멀쩡한데 잘못 막은 횟수"도 같이 세라.

너무 막으면 사용자가 떠난다.

가장 단순한 행동 규칙: 들어갈 때 한 번, 나올 때 한 번 검사한다. 막은 만큼 잘못 막은 것도 센다.


3. 라우터와 캐싱 — 알맞은 곳으로 보내고, 같은 답은 적어 둔다

이 두 겹은 앱이 커질 때 속도와 비용을 지키는 겹이다.

먼저 라우터, 그다음 캐싱을 본다.

망가지는 장면

모든 질문을 가장 크고 비싼 모델 하나에 다 보냈다.

"영업시간이요?" 같은 쉬운 질문에도 비싼 모델이 돌아 청구서가 폭발했다.

게다가 똑같은 질문이 하루에 천 번 와도 매번 새로 답을 만들었다.

일상비유

라우터는 병원 접수처다. 가벼운 증상은 동네 의원으로, 무거운 증상만 큰 병원으로 보낸다.

캐싱은 벽에 붙인 메모지다. 자주 묻는 답은 적어 뒀다가 그대로 읽어 준다.

비유 코드 위험
접수처가 나눠 보냄 (라우터) send_to(classify(question)) 안전 — 쉬운 건 싼 곳, 어려운 건 비싼 곳
메모지 재사용 (캐싱) if q in memo: return memo[q] 안전 — 같은 질문은 즉시·공짜로 답
다 비싼 모델로 매번 새로 model_big.ask(question) 청구서 폭발 + 느림

한 문장 정의 — 라우터는 질문 종류를 보고 알맞은 곳으로 보내는 교통정리이고, 캐싱은 한 번 만든 답을 적어 뒀다가 같은 질문에 재사용해 속도와 비용을 아끼는 겹이다.

예시 폭격

예시 1 — 완성예 (라우터가 나누는 모습)

질문 종류를 보고 보낼 곳을 정한다.

# 비밀번호 문제는 안내 페이지, 청구 문제는 사람에게
kind = classify(question)
if kind == "비밀번호":
    return faq_page()
elif kind == "청구오류":
    return human_agent()

쉬운 건 사람 손 안 빌리고, 무거운 건 사람에게 간다.

예시 2 — 부분완성 (캐싱을 채워 보자)

같은 질문이 또 오면 적어 둔 답을 쓰자.

if question in memo:
    return ________            # 빈칸: memo[question]
answer = model.ask(question)
memo[question] = answer        # 새로 만든 답을 적어 둠

빈칸은 memo[question]이다.

적어 둔 답을 그대로 꺼내 주면 다시 만들 필요가 없다.

예시 3 — 독립적용 (캐싱하면 안 되는 질문)

"오늘 날씨 어때?" 라는 질문을 메모지에 적어 둬도 될까?

안 된다.

날씨는 매일 바뀐다.

어제 적어 둔 답을 오늘 꺼내 주면 틀린 답이 된다.

시간 따라 바뀌는 질문, 사람마다 다른 질문(예: "내 주문 어디쯤?")은 적어 두면 안 된다.

미니 시나리오 — 이럴 때 이렇게

사용자 A의 개인정보가 담긴 답을 일반 질문 답인 줄 알고 메모지에 적었을 때.

→ 나중에 B가 같은 질문을 하면 A의 정보가 튀어나온다.

사람마다 다른 답은 절대 공용 메모지에 적지 마라.

가장 단순한 행동 규칙: 쉬운 질문은 싼 곳으로 보낸다. 안 바뀌는 답만 적어 둔다.


4. 반응 줍기 — 사용자는 행동으로도 말한다

망가지는 장면

앱을 1년 돌렸는데 사용자가 만족하는지 아닌지 아무도 모른다.

"별점 남겨 주세요" 버튼을 달았지만 아무도 안 누른다.

그런데 사람들은 사실 매일 신호를 보내고 있었다.

답을 직접 고쳐 쓰고, 다시 만들기를 누르고, 화면을 그냥 닫고 나갔다.

그 행동을 줍지 않았던 것뿐이다.

일상비유

식당 사장이 손님 반응을 읽는 법.

설문지(별점)는 잘 안 써 준다.

하지만 음식을 남겼는지, 다시 왔는지, 친구를 데려왔는지 같은 행동은 거짓말을 안 한다.

말로 한 칭찬보다 행동이 더 솔직하다.

비유 코드 위험
설문지 (대놓고 묻기) rating = ask_user("몇 점?") 잘 안 써 줌 — 양은 적지만 뜻은 또렷
남긴 음식 보기 (행동 읽기) if user_edited(answer): 비선호 양 많고 솔직 — 단 뜻 해석이 헷갈림
아무것도 안 봄 pass 좋은지 나쁜지 영영 모름

한 문장 정의 — 반응 줍기는 사용자의 대놓고 한 답(별점·좋아요)뿐 아니라 행동(고쳐 쓰기·다시 만들기·그냥 나가기)에서 만족·불만족을 읽어, 모델을 키우는 자료로 모으는 일이다.

예시 폭격

예시 1 — 완성예 (가장 강한 신호: 직접 고쳐 쓰기)

사용자가 모델이 쓴 글을 직접 고치면, 원래 글은 별로였고 고친 글이 더 낫다는 뜻이다.

# 사용자가 답을 고치면: 원래=별로, 고친 것=더 나음
if user_edited(answer):
    bad = answer              # 원래 답은 비선호
    good = user_edit          # 고친 답은 선호

이건 가장 솔직하고 귀한 신호다.

예시 2 — 부분완성 (그냥 나감을 채워 보자)

사용자가 답이 나오는 중에 멈추고 앱을 닫았다.

이건 만족일까 불만족일까?

if user_left_early(session):
    signal = "________"      # 빈칸: 불만족(잘 안 됐을 가능성)

빈칸은 불만족이다.

대화가 잘 풀렸다면 보통 끝까지 본다.

중간에 나갔다면 뭔가 잘 안 됐을 가능성이 크다.

예시 3 — 독립적용 (다시 만들기의 뜻)

돈을 더 내야 하는 앱에서, 사용자가 돈을 내면서까지 다시 만들기를 눌렀다.

이건 약한 신호일까, 강한 신호일까?

강한 불만족 신호다.

공짜면 그냥 눌러 볼 수도 있지만, 돈을 더 쓰면서 다시 만들었다는 건 첫 답이 정말 별로였다는 뜻이다.

미니 시나리오 — 이럴 때 이렇게

별점이 거의 안 모일 때.

→ 별점에 매달리지 말고, 고쳐 쓰기·다시 만들기·조기 종료 같은 행동을 세라.

행동은 별점보다 훨씬 많고 솔직하다.

가장 단순한 행동 규칙: 별점만 기다리지 말고 행동을 읽어라. 직접 고쳐 쓴 것이 가장 강한 신호다.


5. 반응은 비뚤다 — 그대로 믿으면 안 된다

망가지는 장면

사용자 반응을 그대로 모아 모델을 다시 가르쳤다.

그랬더니 모델이 정확한 답보다 듣기 좋은 답을 하기 시작했다.

사람들이 듣고 싶은 말에 좋아요를 눌렀고, 모델이 그걸 그대로 배운 것이다.

반응을 의심 없이 믿은 대가였다.

일상비유

별점은 거짓말을 조금씩 한다.

택시 기사 평점이 다들 4.8점인 이유는, 손님이 미안하고 귀찮아서 후하게 주기 때문이다.

두 답을 나란히 보여 주면 사람은 왼쪽(첫째)을 더 누른다. 읽기 귀찮아서 그냥 앞엣것을 고른다.

반응은 진실의 일부일 뿐, 그대로 믿으면 안 된다.

비유 코드 위험
후하게 주는 별점 (미안해서) rating = 4.8 # 다들 높음 실제보다 좋아 보임 — 진짜 불만이 가려짐
첫째를 더 누름 (위치 탓) pick(first) # 읽기 귀찮아서 답 품질이 아니라 자리 때문에 뽑힘
듣고 싶은 말에 좋아요 like(듣기_좋은_답) 모델이 아첨꾼이 됨 — 정확함을 버림

한 문장 정의 — 사용자 반응은 미안해서 후하게 주고(후함), 첫째 것을 더 누르고(자리), 듣고 싶은 답을 더 좋아하는(아첨) 비뚤어짐이 섞여 있어, 그대로 믿지 말고 보정해서 읽어야 한다.

예시 폭격

예시 1 — 완성예 (자리 비뚤어짐 고치기)

두 답 중 첫째를 더 누르는 버릇을 막으려면, 보여 주는 순서를 매번 섞는다.

# 첫째만 눌리는 걸 막으려고 순서를 뒤섞음
options = shuffle([답A, 답B])   # 자리를 무작위로
show(options)

이러면 자리가 아니라 진짜 품질로 뽑힌다.

예시 2 — 부분완성 (후함 비뚤어짐 고치기)

별점이 다들 높게만 나온다.

숫자 대신 말로 된 선택지로 바꿔 보자.

# 숫자 별점 대신 구체적인 말로 물음
# "이 기사와 ________ 매칭하지 마세요" 같은 선택지
choice = ask_user("이 기사와 ____ 매칭하지 마세요")
# 빈칸: 다시는

빈칸은 다시는이다.

"다시는 매칭하지 마세요" 같은 구체적인 말은 미안함에 덜 휘둘려 더 솔직한 답이 나온다.

예시 3 — 독립적용 (아첨을 막으려면)

반응을 그대로 모델에 먹이면 아첨꾼이 된다.

그럼 어떻게 해야 할까?

반응을 바로 학습 자료로 쓰지 말고, 중간에 한 번 거른다.

좋아요가 많이 달린 답이라도, 그게 정말 정확한 답인지 따로 검사한 뒤에 쓴다.

듣기 좋음과 정확함을 갈라서 본다.

미니 시나리오 — 이럴 때 이렇게

수집한 반응이 다 긍정적이라 안심될 때.

→ 안심하지 말고 의심하라. 후함 비뚤어짐 때문에 진짜 불만이 가려졌을 수 있다.

특히 반응을 모델에 먹이기 전에는 한 번 더 거른다.

가장 단순한 행동 규칙: 반응을 그대로 믿지 말고 보정한다. 모델에 먹이기 전에 정확한지 한 번 거른다.


정리

핵심 3줄.

  • 다 만들지 말고 "질문→답" 한 줄에서 시작해, 진짜 문제가 보일 때 가드레일·라우터·캐싱을 한 겹씩 덧댄다.
  • 사용자는 별점뿐 아니라 행동(고쳐 쓰기·다시 만들기·그냥 나가기)으로도 말하니, 그 신호를 주워 모델을 키운다.
  • 반응은 후함·자리·아첨으로 비뚤어져 있으니 그대로 믿지 말고 보정하고, 모델에 먹이기 전에 한 번 거른다.

이걸로 0장에서 시작한 큰 줄기(가져다 → 시키고 → 붙이고 → 채점하고 → 다듬고 → 운영)가 진짜 앱으로 어떻게 합쳐지는지까지 봤습니다. 한 줄에서 시작해 한 겹씩, 그리고 사용자 반응을 의심하며 줍는 것 — 이 두 가지만 들고 가면 됩니다.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플